require 'httparty'
require 'json'
require 'require_all'

require_rel 'AboutYou'

###
# Provides access to the Collins Frontend Platform.
# All the public methods cover a single API query.
#
# author:: Collins GmbH & Co KG
###
class AY
  # environment-named for api-staging
  API_ENVIRONMENT_STAGE   = 'stage'
  # environment-named for api-sandbox
  API_ENVIRONMENT_SANDBOX = 'sandbox'
  # environment-named for api-live
  API_ENVIRONMENT_LIVE    = 'live'
  # url for staging
  IMAGE_URL_STAGE   = 'http://mndb.staging.aboutyou.de/mmdb/file'
  # url for the sandbox
  IMAGE_URL_SANDBOX = 'http://mndb.sandbox.aboutyou.de/mmdb/file'
  # url for live
  IMAGE_URL_LIVE    = 'http://cdn.aboutyou.de/file'

  # the client which performs the api calls
  attr_accessor :about_you_client
  # the app id of the app which should be represented by an instance of AY
  attr_accessor :app_id
  # the authentication token from dev-center for the app id
  attr_accessor :app_password
  # the endpoint used for api calls
  attr_accessor :api_endpoint
  # the environment which should be used by the app
  attr_accessor :environment
  # a logger template
  attr_accessor :logger
  # the model factory builds model objects based on the api response
  attr_accessor :model_factory_instance
  # the url for getting images
  attr_accessor :base_image_url
  # the session id from a user using this app
  attr_accessor :session_id
  # the CacheServer
  attr_accessor :cache

  ###
  # the Constructor for the AY class
  #
  # * *Args*    :
  #   - +app_id+ -> The App-Id of the App
  #   - +app_password+ -> The Auth-Token of the App
  #   - +session_id+ -> A String containing the sessionId of the User
  #   - +api_endpoint+ -> Can be either live or staging
  #   - +resultFactory+ -> If nil it will use the DefaultModelFactory with the DefaultFacetManager
  #   - +logger+ -> Logger-Template
  #   - +cache+ -> The preferred Caching-Strategy
  #
  # * *Returns* :
  #   - Instance of AY
  ###
  def initialize(
      app_id,
      app_password,
      session_id = 'SESSION_ID',
      cache = nil,
      api_endpoint = API_ENVIRONMENT_LIVE,
      result_factory = nil,
      logger = nil
    )

    self.session_id   = session_id
    self.app_id       = app_id
    self.app_password = app_password
    self.about_you_client = AboutYou::SDK::Client.new(
      app_id,
      app_password,
      api_endpoint,
      logger
    )

    self.cache = cache
    if result_factory
      self.model_factory_instance = result_factory
    else
      init_default_factory(self.cache)
    end

    if api_endpoint == API_ENVIRONMENT_STAGE
      self.base_image_url = (IMAGE_URL_STAGE)
      self.environment = API_ENVIRONMENT_STAGE
    elsif api_endpoint == API_ENVIRONMENT_SANDBOX
      self.base_image_url = (IMAGE_URL_SANDBOX)
      self.environment = API_ENVIRONMENT_SANDBOX
    elsif api_endpoint == API_ENVIRONMENT_LIVE
      self.base_image_url = (IMAGE_URL_LIVE)
      self.environment = API_ENVIRONMENT_LIVE
    end
  end

  ###
  # Adds a single item into the basket.
  # You can specify an amount. Please mind, that an amount > 1 will result
  # in #amount basket positions.
  # So if you read out the basket again later, it's your job to
  # merge the positions again.
  #
  # * *Args*    :
  #   - +session_id+ -> A String containing the sessionId of the User
  #   - +variant_id+ -> Id of the Variant which should be added to the basket
  #   - +amount+ -> The Amount of the Item which should be added to the basket
  #
  # * *Fails* :
  #   - if the variant id cant be transformed into an Integer
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::Basket
  ###
  def add_item_to_basket(session_id, variant_id, amount = 1)
    basket = AboutYou::SDK::Model::Basket.new

    unless variant_id.is_a?(Integer)
      if variant_id.is_a?(String) && variant_id[/\d/]
        variant_id = Integer(variant_id)
      else
        fail 'the variant id must be an integer or string with digits'
      end
    end

    amount.times do
      basket.update_item(AboutYou::SDK::Model::Basket::BasketItem.new(
          generate_basket_item_id,
          variant_id
      ))
    end

    update_basket(session_id, basket)
  end

  ###
  # Method to create a unique item id for items in basket
  #
  # * *Args*    :
  #
  # * *Returns* :
  #   - an Intger
  ###
  def generate_basket_item_id
    'i_' + SecureRandom.uuid
  end

  ###
  # Method to remove items from basket
  #
  # * *Args*    :
  #   - +session_id+ -> A String containing the sessionId of the User
  #   - +itemIds+ -> an Array of Item ids which should be removed
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::Basket
  ###
  def remove_items_from_basket(session_id, item_ids)
    basket = AboutYou::SDK::Model::Basket.new
    basket.delete_items(item_ids)

    update_basket(session_id, basket)
  end

  ###
  # Method to update a given basket
  #
  # * *Args*    :
  #   - +session_id+ -> A String containing the sessionId of the User
  #   - +basket+ -> an Instance of a basket
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::Basket
  ###
  def update_basket(session_id, basket)
    query.update_basket(session_id, basket).execute_single
  end

  ###
  # Method to initiaite the order
  #
  # * *Args*    :
  #   - +session_id+ -> A String containing the sessionId of the User
  #   - +successUrl+ -> callback URL if the order was OK
  #   - +cancelUrl+ -> callback URL if the order was canceled [optional]
  #   - +errorUrl+ -> callback URL if the order had any exceptions [optional]
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::InitiateOrder
  ###
  def initiate_order(session_id, success_url, cancel_url = nil, error_url = nil)
    query.initiate_order(
      session_id,
      success_url,
      cancel_url,
      error_url
    ).execute_single
  end

  ###
  # Returns the result of an auto completion API request.
  # Auto completion searches for products and categories by
  # a given prefix (searchword).
  #
  # * *Args*    :
  #   - +searchword+ -> The prefix search word to search for
  #   - +limit+ -> Maximum number of results [optional]
  #   - +types+ -> Array of types to search for [optional]
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::Automcomplete
  ###
  def fetch_autocomplete(
    searchword,
    limit = 50,
    types = [
      AboutYou::SDK::Model::Autocomplete::TYPE_PRODUCTS,
      AboutYou::SDK::Model::Autocomplete::TYPE_CATEGORIES
    ]
  )
    query.fetch_autocomplete(searchword, limit, types).execute_single
  end
  
  ###
  # Returns the result of an spell correction API request.
  # spell correction searches for products by a given searchword.
  # You might filter by category ids aswell.
  #
  # * *Args*    :
  #   - +searchword+ -> The prefix search word to search for
  #   - +category_ids+ -> The category ids used as a filter [optional]
  #
  # * *Fails* :
  #   - if a category id cant be transformed into an Integer
  #   - if a category id is smaller then 1
  #
  # * *Returns* :
  #   - Array of Strings
  ###
  def fetch_spell_correction(searchword, category_ids = nil)
    unless category_ids.nil?
      # we allow to pass a single ID instead of an array
      category_ids = Array(category_ids)

      category_ids.each do |cat_id|
        fail 'InvalidArgumentException! A single category ID must be an integer
          or a numeric string' unless cat_id.is_a?(Integer) || cat_id[/\d/]
        fail '\InvalidArgumentException! A single category ID must be greater
          than 0' if cat_id < 1
      end
    end

    query.fetch_spell_correction(searchword, category_ids).execute_single
  end

  ###
  # Fetch the basket of the given session_id.
  #
  # * *Args*    :
  #   - +session_id+ -> A String containing the sessionId of the User
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::Basket
  ###
  def fetch_basket(session_id)
    query.fetch_basket(session_id).execute_single
  end

  ###
  # Returns the result of a category search API request.
  # By passing one or several category ids it will return
  # a result of the categories data.
  #
  # * *Args*    :
  #   - +ids+ -> either a single category ID as integer or an array of IDs [optional]
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::CategoriesResult
  ###
  def fetch_categories_by_ids(ids = nil)
    # we allow to pass a single ID instead of an array
    ids = Array(ids) if ids

    AboutYou::SDK::Model::CategoriesResult.new(category_manager, ids)
  end

  ###
  # Fetches the Root Categories of the Category Tree
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::CategoryTree
  ###
  def fetch_category_tree
    AboutYou::SDK::Model::CategoryTree.new(category_manager)
  end

  ###
  # The Categories will be fetched automatically, if required by any other fetch method
  #
  # * *Args*    :
  #   - +fetchIfEmpty+ -> defines whether to fetch if empty or not [optional]
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::CategoryManager::DefaultCategoryManager
  ###
  def category_manager(fetch_if_empty = true)
    category_manager = model_factory.category_manager

    if fetch_if_empty && category_manager.empty?
      query.require_category_tree.execute_single
    end

    category_manager
  end

  ###
  # Fetches the products for specific ids
  #
  # * *Args*    :
  #   - +ids+ -> Either a single id or an Array of ids which should be fetched
  #   - +fields+ -> Additional product fields which should be fetched for each product [optional]
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::ProductsResult
  ###
  def fetch_products_by_ids(ids, fields = [])
    # we allow to pass a single ID instead of an array
    ids = Array(ids)
    result = query.fetch_products_by_ids(ids, fields).execute_single
    products_not_found = result.ids_not_found

    if !products_not_found.empty? && logger
      logger.warning('products not found: appid=' + app_id +
          ' product ids=[' + products_not_found.join(',') + ']'
      )
    end

    result
  end

  ###
  # Fetches variants for specific ids
  #
  # * *Args*    :
  #   - +ids+ -> Either a single id or an Array of ids which should be fetched
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::VariantsResult
  ###
  def fetch_variants_by_ids(ids)
    # we allow to pass a single ID instead of an array
    ids = Array(ids)
    result = query.fetch_live_variant_by_ids(ids).execute_single
    variants_not_found = result.variants_not_found

    if result.variants_not_found? && logger
      logger.warning('variants or products for variants not found: appid=' +
          app_id + ' variant ids=[' + variants_not_found.join(',') + ']'
      )
    end

    result
  end

  ###
  # Fetches products for specific eans
  #
  # * *Args*    :
  #   - +eans+ -> Either a single ean or an Array of eans which should be fetched
  #   - +fields+ -> Additional product fields which should be fetched for each product [optional]
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::ProductsEanResult
  ###
  def fetch_products_by_eans(eans, fields = [])
    # we allow to pass a single ID instead of an array
    eans = Array(eans)

    query.fetch_products_by_eans(eans, fields).execute_single
  end

  ###
  # Fetches the response for a product search
  #
  # * *Args*    :
  #   - +criteria+ -> Hash containing one or multiple search terms
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::ProductSearchResult
  ###
  def fetch_product_search(criteria)
    query.fetch_product_search(criteria).execute_single
  end

  ###
  # Fetch the facets of the given group_ids
  #
  # * *Args*    :
  #   - +group_ids+ -> Array of group ids [optional]
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::Facet
  ###
  def fetch_facets(group_ids = [])
    if model_factory_instance.facet_manager.empty?
      query.fetch_facets.execute_single
    end
    model_factory_instance.facet_manager.facets_by_group_ids(group_ids)
  end

  ###
  # Fetches all possible Facet types
  #
  # * *Returns* :
  #   - Array with all group ids
  ###
  def fetch_facet_types
    query.fetch_facet_types.execute_single
  end

  ###
  # fetches Order for specific id
  #
  # * *Args*    :
  #   - +orderId+ -> The id for which an order should be returned
  #
  # * *Returns* :
  #   - Instance of AboutYou::SDK::Model::Order
  ###
  def fetch_order(order_id)
    query.fetch_order(order_id).execute_single
  end

  ###
  # Fetch single facets by id and group id
  #
  # * *Args*    :
  #   - +params+ -> Hash containing 2 keys: "id", "group_id"
  #
  # * *Returns* :
  #   - AboutYou::SDK::Model::Facet
  ###
  def fetch_facet(params)
    query.fetch_facet(params).execute_single
  end

  ###
  # Returns the result of a suggest API request.
  # Suggestions are words that are often searched together
  # with the searchword you pass (e.g. "stretch" for "jeans").
  #
  # * *Args*    :
  #   - +searchword+ -> The search string to search for
  #
  # * *Returns* :
  #   - an Array containing the suggestions
  ###
  def fetch_suggest(searchword)
    query.fetch_suggest(searchword).execute_single
  end

  ###
  # Returns the list of child apps
  #
  # * *returns*  :
  #   - an Array containing all child Apps
  ###
  def fetch_child_apps
    query.fetch_child_apps.execute_single
  end

  ###
  # Method for getting the query
  #
  # * *Returns* :
  #   - an Instance of AboutYou::SDK::Query
  ###
  def query
    AboutYou::SDK::Query.new(about_you_client, model_factory)
  end

  ###
  # Method for getting the model factory. If no model factory is set it initializes the default one
  #
  # * *Returns* :
  #   - an Instance of AboutYou::SDK::Factory::DefaultModelFactory
  ###
  def model_factory
    init_default_factory unless model_factory_instance

    model_factory_instance
  end

  ###
  # Method for getting the product search criteria base class, on which you can specify your criteria
  #
  # * *Args*    :
  #   - +session_id+ -> a string containing the session id [optional]
  #
  # * *Returns* :
  #   - AboutYou::SDK::Criteria::ProductSearchCriteria
  ###
  def product_search_criteria(session_id = nil)
    session_id  = self.session_id unless session_id

    AboutYou::SDK::Criteria::ProductSearchCriteria.new(session_id)
  end

  ###
  # Returns the URL to the Collins JavaScript file for helper functions
  # to add product variants into the basket of ABOUT YOU or auto-resizing
  # the iframe. This URL may be changed in future, so please use this method instead
  # of a hard coded URL into your HTML template.
  #
  # * *Returns* :
  #   - a string containing an url to the javascript file
  ###
  def java_script_url
    if environment == API_ENVIRONMENT_STAGE
      '//devcenter-staging-www1.pub.collins.kg:81/appjs/' +
        String(app_id) + '.js'
    else
      '//developer.aboutyou.de/appjs/' + String(app_id) + '.js'
    end
  end

  ###
  # Returns a HTML script tag that loads the Collins JavaScript fie.
  #
  # * *Returns* :
  #   - a string containing an HTML script tag
  ###
  def java_script_tag
    '<script type="text/javascript" src="' + java_script_url + '"></script>'
  end

  ###
  # Setter-Method for the app-Credentials
  #
  # * *Args*    :
  #   - +app_id+ -> the id of an app
  #   - +app_password+ -> the auth-token for the app
  ###
  def app_credentials=(app_id, app_password)
    self.app_id                   = app_id
    self.app_password             = app_password
    about_you_client.app_id       = app_id
    about_you_client.app_password = app_password
  end

  ###
  # Setter-Method for the api endpoint
  #
  # * *Args*    :
  #   - +api_endpoint+ -> the endpoint can be the string 'stage' or 'live', then the default endpoints will be used or an absolute url
  ###
  def api_endpoint=(api_endpoint)
    about_you_client.api_endpoint = api_endpoint
  end

  ###
  # Setter-Method for the base image url
  #
  # * *Args*    :
  #   - +base_image_url+ -> nil will reset to the default url, false to get relative urls, otherwise the url prefix
  ###
  def base_image_url=(base_image_url = nil)
    if !base_image_url
      @base_image_url = IMAGE_URL_LIVE
    elsif base_image_url.is_a?(String)
      @base_image_url = base_image_url.gsub(/[#{'/'}]+$/, '')
    else
      @base_image_url = ''
    end

    model_factory.base_image_url = @base_image_url
  end

  ###
  # initializes the default model factory
  #
  # * *Args*    :
  #   - +cache+ -> an Instance of the used cache [optional]
  ###
  def init_default_factory(cache = nil)
    self.model_factory_instance = AboutYou::SDK::Factory::DefaultModelFactory.new(self)

    model_factory_instance.initialize_managers(
      AboutYou::SDK::Model::FacetManager::DefaultFacetManager.new(
        cache,
        app_id,
        self
      ),
      AboutYou::SDK::Model::CategoryManager::DefaultCategoryManager.new(
        cache,
        app_id,
        self
      )
    )
    model_factory_instance.base_image_url = base_image_url
  end
end
